今天,我們要來為我們的系統加上重要的安全機制,並且優化整體的用戶體驗。
首先,我們要實現一個完整的身份驗證機制。我們將使用 JWT(JSON Web Token)來進行身份驗證。
npm install jsonwebtoken axios
npm install -D @types/jsonwebtoken
在 src/contexts/AuthContext.tsx
中:
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';
interface User {
id: number;
username: string;
role: string;
}
interface AuthContextType {
user: User | null;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// 這裡可以添加驗證 token 有效性的邏輯
}
}, []);
const login = async (username: string, password: string) => {
try {
const response = await axios.post('/api/login', { username, password });
const { user, token } = response.data;
localStorage.setItem('token', token);
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setUser(user);
} catch (error) {
console.error('登入失敗', error);
throw error;
}
};
const logout = () => {
localStorage.removeItem('token');
delete axios.defaults.headers.common['Authorization'];
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth 必須在 AuthProvider 內使用');
}
return context;
};
在 src/pages/Login.tsx
中:
import React from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
interface LoginForm {
username: string;
password: string;
}
const Login: React.FC = () => {
const { register, handleSubmit } = useForm<LoginForm>();
const navigate = useNavigate();
const { login } = useAuth();
const onSubmit = async (data: LoginForm) => {
try {
await login(data.username, data.password);
navigate('/dashboard');
} catch (error) {
console.error('登入失敗', error);
// 這裡可以添加錯誤處理,比如顯示錯誤訊息
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<Card className="w-[350px]">
<CardHeader>
<CardTitle>歡迎來到 Gym Pro</CardTitle>
<CardDescription>請登入以繼續</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-4">
<Input {...register('username')} placeholder="使用者名稱" required />
<Input {...register('password')} type="password" placeholder="密碼" required />
</div>
<Button className="w-full mt-4" type="submit">登入</Button>
</form>
</CardContent>
</Card>
</div>
);
};
export default Login;
接下來,我們要實現基於角色的存取控制,確保只有具有適當權限的用戶才能訪問特定頁面或執行特定操作。
在 src/components/PrivateRoute.tsx
中:
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
interface PrivateRouteProps {
children: React.ReactNode;
requiredRole?: string;
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children, requiredRole }) => {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRole && user.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
};
export default PrivateRoute;
在 src/App.tsx
中:
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import PrivateRoute from './components/PrivateRoute';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';
import Members from './pages/Members';
import MemberDetail from './pages/MemberDetail';
import Classes from './pages/Classes';
import ClassCalendar from './pages/ClassCalendar';
import ClassDetail from './pages/ClassDetail';
import Reports from './pages/Reports';
import Login from './pages/Login';
import Unauthorized from './pages/Unauthorized';
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/unauthorized" element={<Unauthorized />} />
<Route path="/" element={<PrivateRoute><MainLayout /></PrivateRoute>}>
<Route index element={<Dashboard />} />
<Route path="members" element={<PrivateRoute requiredRole="admin"><Members /></PrivateRoute>} />
<Route path="members/:id" element={<PrivateRoute requiredRole="admin"><MemberDetail /></PrivateRoute>} />
<Route path="classes" element={<Classes />} />
<Route path="class-calendar" element={<ClassCalendar />} />
<Route path="classes/:id" element={<PrivateRoute requiredRole="trainer"><ClassDetail /></PrivateRoute>} />
<Route path="reports" element={<PrivateRoute requiredRole="admin"><Reports /></PrivateRoute>} />
</Route>
</Routes>
</Router>
</AuthProvider>
);
}
export default App;
為了提供更好的用戶體驗,我們應該實現全局錯誤處理。
在 src/components/ErrorBoundary.tsx
中:
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(_: Error): State {
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return <h1>抱歉,出了點問題。請稍後再試。</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
更新 src/App.tsx
:
import ErrorBoundary from './components/ErrorBoundary';
function App() {
return (
<ErrorBoundary>
<AuthProvider>
{/* ... 其他代碼 ... */}
</AuthProvider>
</ErrorBoundary>
);
}
最後,讓增加一些小細節來改善整體的用戶體驗。
建立一個 src/components/LoadingSpinner.tsx
:
import React from 'react';
const LoadingSpinner: React.FC = () => (
<div className="flex justify-center items-center h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
</div>
);
export default LoadingSpinner;
在需要載入數據的組件中使用這個 LoadingSpinner。
使用 react-toastify 來增加反饋訊息:
npm install react-toastify
在 src/App.tsx
中:
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function App() {
return (
<ErrorBoundary>
<AuthProvider>
{/* ... 其他代碼 ... */}
<ToastContainer />
</AuthProvider>
</ErrorBoundary>
);
}
然後在需要顯示反饋訊息的地方使用 toast
函數。
今天我們為 Gym Pro 系統增加了安全機制和用戶體驗改進,包括: